#!/usr/bin/perl
#
#	Copyright (C) 2000, Andrew Arensburger.
#	You may distribute this file under the terms of the Artistic
#	License, as specified in the README file.
#
# $Id: std-categories,v 3.4 2000-12-10 01:13:25 arensb Exp $
#
# This is a ColdSync Sync conduit for synchronizing the categories between
# a database on the Palm and one on the desktop.
#
# This script is rather hairy, and probably doesn't do the Right Thing in
# all cases, but that's because categories aren't quite as clean as they
# could be. Certainly nowhere near as pretty as record syncing.

# XXX - "std-categories -config" gives a dangerous configuration:
#	type: */*;
# this includes databases that don't have a standard AppInfo block.

use strict;
use ColdSync;
use ColdSync::SPC;
use Palm::StdAppInfo;

use vars qw( %Pappinfo $DEBUG );
use constant NumCategories => Palm::StdAppInfo::numCategories;
					# Number of categories in a
					# standard AppInfo block

$DEBUG = 1;		# Turns debugging on

StartConduit("sync");
select STDERR;		# XXX - Just for debugging

my $err;
my $dbinfo;		# Information about currently-open database
my $dbh;		# Database handle

# Find out which database we're dealing with
$dbinfo = spc_get_dbinfo;
die "401 Error reading database info" if !defined($dbinfo);

# Open the database
($dbh = &dlp_OpenDB($dbinfo->{name}, 0x80))
	or die "402 Can't open database";

# Get the category list from the Palm
use vars qw( $Pappinfo_raw );	# Raw AppInfo block data
use vars qw( %Pappinfo );	# Parsed category information from AppInfo
				# block

$Pappinfo_raw = dlp_ReadAppBlock($dbh);
				# Download the AppInfo block from the Palm
# XXX - Error-checking
# XXX - Might be a good idea to make sure that $Pappinfo_raw is at least
# long enough for a standard set of categories, in case some dumbass runs
# this on a non-StdAppInfo database.
&Palm::StdAppInfo::parse_StdAppInfo(\%Pappinfo, $Pappinfo_raw);
				# Parse the AppInfo block into a set of
				# categories.
# XXX - Might be a good idea to make sure that category 0 has ID 0, in case
# some dumbass runs this on a non-StdAppInfo database.

if ($DEBUG)
{
	print "* Palm categories:\n";
	&dumphash(\%Pappinfo, "");
	print "* Desktop categories:\n";
	&dumphash($PDB->{appinfo}, "");
}

# Phase 1: Underpants!

# Phase 2: Get the names and IDs in sync between the Palm and Desktop
# Phase 2.1: Look at the categories on the Palm
print STDERR "===== Examining Palm categories:\n" if $DEBUG;
for (my $i = 0; $i < NumCategories; $i++)
{
	my $category = $Pappinfo{categories}[$i];
	my $Dcategory;

	print STDERR "category $i: [$category->{name}]\n" if $DEBUG;

	# XXX - This logic for finding the corresponding category on the
	# desktop is too primitive: if the Palm category is unchanged, then
	# we need to find either a clean category on the desktop with the
	# same name, or a modified category with the same ID.

	if ($category->{name} eq "")
	{
		$Dcategory = &find_cat_by_id($PDB->{appinfo},
					     $category->{id});

		if (!defined($Dcategory))
		{
			# This category exists neither on the Palm, nor
			# on the desktop. Nothing to do.
			print STDERR "This category doesn't exist. Nothing to do.\n"
				if $DEBUG;

			next;
		}

		# This category doesn't exist on the Palm, but does
		# exist on the desktop
		if ($Dcategory->{renamed})
		{

			# Doesn't exist on the Palm.
			# Exists on the desktop.
			# Renamed on the desktop.
			# -> Add it on the Palm.
			print STDERR "Renamed on the desktop. Renaming on Palm.\n"
				if $DEBUG;

			$category->{name} = $Dcategory->{name};
			$category->{renamed} = 0;
			$Dcategory->{renamed} = 0;
			$Dcategory->{_moveto} = $i;
		} else {

			# Doesn't exist on Palm.
			# Exists on desktop.
			# Not renamed.
			# -> Deleted on Palm. Delete it on desktop
			print STDERR "Deleted on Palm. Deleting on desktop.\n"
				if $DEBUG;

			$Dcategory->{name} = "";
			$category->{renamed} = 0;
			$Dcategory->{renamed} = 0;
			$Dcategory->{_moveto} = $i;
		}
	} else {
		# This category exists on the Palm

		if ($category->{renamed})
		{
			$Dcategory = &find_cat_by_id($PDB->{appinfo},
						     $category->{id});

			if (defined($Dcategory))
			{
				# Exists on Palm.
				# Exists on desktop.
				# Renamed on Palm.
				# -> Renamed on Palm. Change name on desktop.
				print STDERR "Renamed on Palm. Need to rename on desktop.\n"
					if $DEBUG;

				$Dcategory->{name} = $category->{name};
				$category->{renamed} = 0;
				$Dcategory->{renamed} = 0;
				$Dcategory->{_moveto} = $i;
			} else {
				# Exists on Palm.
				# Doesn't exist on desktop.
				# Renamed on Palm.
				# -> Add it on the desktop.
				print STDERR "New category. Need to add on desktop.\n"
					if $DEBUG;

				$category->{renamed} = 0;
				$Dcategory->{renamed} = 0;
				$Dcategory->{_moveto} = $i;
			}
		} else {

			# XXX - The category might exist, renamed, on the
			# desktop, in which case &find_cat_by_name won't
			# find it.
			$Dcategory = &find_cat_by_name($PDB->{appinfo},
						       $category->{name});

			if (defined($Dcategory))
			{
				# This category exists on the Palm and
				# on the desktop. It has not been renamed
				if ($Dcategory->{renamed})
				{
					# Exists on Palm.
					# Exists on desktop.
					# Not renamed on Palm.
					# Renamed on desktop.
					# -> Rename on Palm.
					print STDERR "Renamed on desktop. Need to rename on Palm.\n"
						if $DEBUG;
					$category->{name} = $Dcategory->{name};
					$category->{renamed} = 0;
					$Dcategory->{renamed} = 0;
					$Dcategory->{_moveto} = $i;
				} else {
					# Exists on Palm.
					# Exists on desktop.
					# Not renamed on Palm.
					# Not renamed on desktop.
					# -> Okay, but make sure the IDs are
					#    in sync.

					print STDERR "OK. Make sure IDs are in sync.\n"
						if $DEBUG;

					$Dcategory->{id} = $category->{id};
					$category->{renamed} = 0;
					$Dcategory->{renamed} = 0;
					$Dcategory->{_moveto} = $i;
				}
			} else {
				# Exists on Palm.
				# Doesn't exist on desktop.
				# Not renamed on Palm.
				# -> Not clear whether a) deleted on desktop,
				#    or b) added (and synced) on Palm.
				# -> Be conservative and add it on the desktop.

				print STDERR "Judgment call: add on the desktop.\n"
					if $DEBUG;
			}
		}
	}
}

# Phase 2.2: Look at the categories on the desktop
print STDERR "===== Examining desktop categories:\n" if $DEBUG;
for (my $i = 0; $i < NumCategories; $i++)
{
	my $category = $PDB->{appinfo}{categories}[$i];

	print STDERR "category $i: [$category->{name}]\n" if $DEBUG;
	if (defined($category->{_moveto}))
	{
		print STDERR "\tSeen it already.\n" if $DEBUG;
		next;
	}

	if ($category->{renamed})
	{
		# This category exists on the desktop, but not on the Palm.
		if ($category->{name} eq "")
		{
			# Doesn't exist on Palm.
			# Exists on desktop.
			# Deleted on desktop
			# -> Delete on Palm.

			# XXX - Is there anything to do, really?
			print STDERR "Deleted on desktop. Deleting on Palm.\n"
				if $DEBUG;

			$category->{_moveto} = 0;
		} else {
			# Doesn't exist on Palm.
			# Exists on desktop.
			# Renamed on desktop.
			# -> Add to Palm
			print STDERR "Added on desktop. Adding on Palm.\n"
				if $DEBUG;

			my $newindex;

			if ($Pappinfo{categories}[$i]{name} eq "")
			{
				# This index is free on the Palm, so we
				# don't have to move the category.
				$newindex = $i;
			} else {
				$newindex = &find_empty_cat(\%Pappinfo);
				if ($newindex < 0)
				{
					# No free slots available.
					print STDERR "No free categories. Dropping desktop category \"$category->{name}\".\n"
						if $DEBUG;

					$category->{_moveto} = 0;
					next;
				}
			}

			print STDERR "Adding on Palm, index $newindex\n"
				if $DEBUG;

			my $Pcategory = $Pappinfo{categories}[$newindex];

			$Pcategory->{name} = $category->{name};
			$Pcategory->{id} = $category->{id};
			$Pcategory->{renamed} = 0;
			$category->{_moveto} = $i;
		}
	} else {
		# This category exists on the desktop, but not on the Palm.
		# It has not been renamed.
		# It may have been either added (and synced) on the desktop,
		# or it might have been deleted on the Palm.
		# -> Be conservative, and add it on the Palm, if possible.

		my $newindex;

		if ($Pappinfo{categories}[$i]{name} eq "")
		{
			# This index is free on the Palm, so we don't have
			# to move the category.
			$newindex = $i;
		} else {
			$newindex = &find_empty_cat(\%Pappinfo);
			if ($newindex < 0)
			{
				# No free slots available.
				print STDERR "No free categories. Dropping desktop category \"$category->{name}\".\n"
					if $DEBUG;

				$category->{_moveto} = 0;
				next;
			}
		}

		print STDERR "Adding on Palm, index $newindex\n"
			if $DEBUG;

		my $Pcategory = $Pappinfo{categories}[$newindex];

		$Pcategory->{name} = $category->{name};
		$Pcategory->{id} = $category->{id};
		$Pcategory->{renamed} = 0;
		$category->{_moveto} = $i;
	}
}

# XXX - Should probably double-check that category 0 has ID 0 (Unfiled).

if ($DEBUG)
{
	print STDERR "===== Final set of categories:\n";
	&dumphash(\%Pappinfo, "");
}

# Phase 2.3: Move new categories to the Palm.
for (@{$PDB->{records}})
{
	my $oldcat = $_->{category};
	my $newcat = $PDB->{appinfo}{categories}[$oldcat]{_moveto};

	if (!defined($newcat))
	{
		die("555 Category $oldcat doesn't have a _moveto. Aborting.");
	}

	next if $oldcat == $newcat;

	print STDERR "Moving record $_->{id} from $oldcat to $newcat.\n"
		if $DEBUG;

	$_->{category} = $newcat;
	$_->{attributes}{dirty} = 1;
}

# Phase 2.4: Copy the Palm AppInfo block to the desktop PDB
for (my $i = 0; $i < NumCategories; $i++)
{
	my $Pcategory = $Pappinfo{categories}[$i];
	my $Dcategory = $PDB->{appinfo}{categories}[$i];

	$Dcategory->{name}	= $Pcategory->{name};
	$Dcategory->{id}	= $Pcategory->{id};
	$Dcategory->{renamed}	= $Pcategory->{renamed};
}

# Phase 3: Profits!

# Upload the AppInfo block to the Palm.
$Pappinfo_raw = &Palm::StdAppInfo::pack_StdAppInfo(\%Pappinfo);
#$err = &dlp_WriteAppBlock($dbh, $Pappinfo_raw);
# XXX - Error-checking
$err = &dlp_CloseDB($dbh);
# XXX - Error-checking

#die "599 Dying for no good reason";
		# XXX - Just to avoid writing the database prematurely
EndConduit;

sub find_cat_by_name
{
	my $appinfo = shift;
	my $name = shift;

	for (@{$appinfo->{categories}})
	{
		next unless $_->{name} eq $name;
		return $_;		# Found it
	}

	return undef;			# Can't find it
}

sub find_cat_by_id
{
	my $appinfo = shift;
	my $id = shift;

	for (@{$appinfo->{categories}})
	{
		next unless $_->{id} eq $id;
		return $_;		# Found it
	}

	return undef;			# Can't find it
}

sub find_empty_cat
{
	my $appinfo = shift;
	my $i;

	for ($i = 0; $i < NumCategories; $i++)
	{
		next unless $appinfo->{categories}[$i]{name} eq "";
		return $i;
	}

	return -1;
}

##### Debugging functions

sub hexdump
{
	my $prefix = shift;	# What to print in front of each line
	my $data = shift;	# The data to dump
	my $maxlines = shift;	# Max # of lines to dump
	my $offset;		# Offset of current chunk

	for ($offset = 0; $offset < length($data); $offset += 16)
	{
		my $hex;		# Hex values of the data
		my $ascii;		# ASCII values of the data
		my $chunk;		# Current chunk of data

		last if defined($maxlines) && ($offset >= ($maxlines * 16));

		$chunk = substr($data, $offset, 16);

		($hex = $chunk) =~ s/./sprintf "%02x ", ord($&)/ges;

		($ascii = $chunk) =~ y/\040-\176/./c;

		printf "%s %-48s|%-16s|\n", $prefix, $hex, $ascii;
	}
}

sub dumphash
{
	my $hash = shift;
	my $indent = shift;
	my $key;
	my $value;

	while (($key, $value) = each %{$hash})
	{
		if (ref($value) eq "HASH")
		{
			print $indent, $key, ":\n";
			&dumphash($value, $indent . "\t");
		} elsif (ref($value) eq "ARRAY")
		{
			print $indent, $key, ":\n";
			&dumparray($value, $indent . "\t");
		} else {
			print $indent, $key, " -> [", $value, "]\n";
		}
	}
}

sub dumparray
{
	my $array = shift;
	my $indent = shift;
	my $i;

	for ($i = 0; $i <= $#{$array}; $i++)
	{
		if (ref($array->[$i]) eq "HASH")
		{
			print $indent, $i, ":\n";
			&dumphash($array->[$i], $indent . "\t");
		} elsif (ref($array->[$i]) eq "ARRAY")
		{
			print $indent, $i, ":\n";
			&dumparray($array->[$i], $indent . "\t");
		} else {
			print $indent, $i, ": ", $array->[$i], "\n";
		}
	}
}

# This is for Emacs's benefit:
# Local Variables:	***
# fill-column:	75	***
# End:			***
